{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:33:55.494456Z",
     "start_time": "2020-04-03T12:33:55.489588Z"
    }
   },
   "outputs": [],
   "source": [
    "def warn(*args, **kwargs):\n",
    "    pass\n",
    "import warnings\n",
    "warnings.warn = warn"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:33:56.164445Z",
     "start_time": "2020-04-03T12:33:56.127681Z"
    },
    "colab": {},
    "colab_type": "code",
    "id": "4c7Knmsem0yC"
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import os\n",
    "from time import time\n",
    "import cv2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "dXaDqQ6Nm0yH"
   },
   "source": [
    "### Build Data Loader"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:33:59.156073Z",
     "start_time": "2020-04-03T12:33:59.125478Z"
    },
    "colab": {},
    "colab_type": "code",
    "id": "qPLzEcGtm0yI"
   },
   "outputs": [],
   "source": [
    "from keras.utils import Sequence\n",
    "from keras.utils import np_utils\n",
    "\n",
    "class DataGenerator(Sequence):\n",
    "    \"\"\"Data Generator inherited from keras.utils.Sequence\n",
    "    Args: \n",
    "        directory: the path of data set, and each sub-folder will be assigned to one class\n",
    "        batch_size: the number of data points in each batch\n",
    "        shuffle: whether to shuffle the data per epoch\n",
    "    Note:\n",
    "        If you want to load file with other data format, please fix the method of \"load_data\" as you want\n",
    "    \"\"\"\n",
    "    def __init__(self, directory, batch_size=1, shuffle=True, data_augmentation=True):\n",
    "        # Initialize the params\n",
    "        self.batch_size = batch_size\n",
    "        self.directory = directory\n",
    "        self.shuffle = shuffle\n",
    "        self.data_aug = data_augmentation\n",
    "        # Load all the save_path of files, and create a dictionary that save the pair of \"data:label\"\n",
    "        self.X_path, self.Y_dict = self.search_data() \n",
    "        # Print basic statistics information\n",
    "        self.print_stats()\n",
    "        return None\n",
    "        \n",
    "    def search_data(self):\n",
    "        X_path = []\n",
    "        Y_dict = {}\n",
    "        # list all kinds of sub-folders\n",
    "        self.dirs = sorted(os.listdir(self.directory))\n",
    "        one_hots = np_utils.to_categorical(range(len(self.dirs)))\n",
    "        for i,folder in enumerate(self.dirs):\n",
    "            folder_path = os.path.join(self.directory,folder)\n",
    "            for file in os.listdir(folder_path):\n",
    "                file_path = os.path.join(folder_path,file)\n",
    "                # append the each file path, and keep its label  \n",
    "                X_path.append(file_path)\n",
    "                Y_dict[file_path] = one_hots[i]\n",
    "        return X_path, Y_dict\n",
    "    \n",
    "    def print_stats(self):\n",
    "        # calculate basic information\n",
    "        self.n_files = len(self.X_path)\n",
    "        self.n_classes = len(self.dirs)\n",
    "        self.indexes = np.arange(len(self.X_path))\n",
    "        np.random.shuffle(self.indexes)\n",
    "        # Output states\n",
    "        print(\"Found {} files belonging to {} classes.\".format(self.n_files,self.n_classes))\n",
    "        for i,label in enumerate(self.dirs):\n",
    "            print('%10s : '%(label),i)\n",
    "        return None\n",
    "    \n",
    "    def __len__(self):\n",
    "        # calculate the iterations of each epoch\n",
    "        steps_per_epoch = np.ceil(len(self.X_path) / float(self.batch_size))\n",
    "        return int(steps_per_epoch)\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        \"\"\"Get the data of each batch\n",
    "        \"\"\"\n",
    "        # get the indexs of each batch\n",
    "        batch_indexs = self.indexes[index*self.batch_size:(index+1)*self.batch_size]\n",
    "        # using batch_indexs to get path of current batch\n",
    "        batch_path = [self.X_path[k] for k in batch_indexs]\n",
    "        # get batch data\n",
    "        batch_x, batch_y = self.data_generation(batch_path)\n",
    "        return batch_x, batch_y\n",
    "\n",
    "    def on_epoch_end(self):\n",
    "        # shuffle the data at each end of epoch\n",
    "        if self.shuffle == True:\n",
    "            np.random.shuffle(self.indexes)\n",
    "\n",
    "    def data_generation(self, batch_path):\n",
    "        # load data into memory, you can change the np.load to any method you want\n",
    "        batch_x = [self.load_data(x) for x in batch_path]\n",
    "        batch_y = [self.Y_dict[x] for x in batch_path]\n",
    "        # transfer the data format and take one-hot coding for labels\n",
    "        batch_x = np.array(batch_x)\n",
    "        batch_y = np.array(batch_y)\n",
    "        return batch_x, batch_y\n",
    "      \n",
    "    def normalize(self, data):\n",
    "        mean = np.mean(data)\n",
    "        std = np.std(data)\n",
    "        return (data-mean) / std\n",
    "    \n",
    "    def random_flip(self, video, prob):\n",
    "        s = np.random.rand()\n",
    "        if s < prob:\n",
    "            video = np.flip(m=video, axis=2)\n",
    "        return video    \n",
    "    \n",
    "    def uniform_sampling(self, video, target_frames=64):\n",
    "        # get total frames of input video and calculate sampling interval \n",
    "        len_frames = int(len(video))\n",
    "        interval = int(np.ceil(len_frames/target_frames))\n",
    "        # init empty list for sampled video and \n",
    "        sampled_video = []\n",
    "        for i in range(0,len_frames,interval):\n",
    "            sampled_video.append(video[i])     \n",
    "        # calculate numer of padded frames and fix it \n",
    "        num_pad = target_frames - len(sampled_video)\n",
    "        if num_pad>0:\n",
    "            padding = [video[i] for i in range(-num_pad,0)]\n",
    "            sampled_video += padding     \n",
    "        # get sampled video\n",
    "        return np.array(sampled_video, dtype=np.float32)\n",
    "    \n",
    "    def dynamic_crop(self, video):\n",
    "        # extract layer of optical flow from video\n",
    "        opt_flows = video[...,3]\n",
    "        # sum of optical flow magnitude of individual frame\n",
    "        magnitude = np.sum(opt_flows, axis=0)\n",
    "        # filter slight noise by threshold \n",
    "        thresh = np.mean(magnitude)\n",
    "        magnitude[magnitude<thresh] = 0\n",
    "        # calculate center of gravity of magnitude map and adding 0.001 to avoid empty value\n",
    "        x_pdf = np.sum(magnitude, axis=1) + 0.001\n",
    "        y_pdf = np.sum(magnitude, axis=0) + 0.001\n",
    "        # normalize PDF of x and y so that the sum of probs = 1\n",
    "        x_pdf /= np.sum(x_pdf)\n",
    "        y_pdf /= np.sum(y_pdf)\n",
    "        # randomly choose some candidates for x and y \n",
    "        x_points = np.random.choice(a=np.arange(224), size=10, replace=True, p=x_pdf)\n",
    "        y_points = np.random.choice(a=np.arange(224), size=10, replace=True, p=y_pdf)\n",
    "        # get the mean of x and y coordinates for better robustness\n",
    "        x = int(np.mean(x_points))\n",
    "        y = int(np.mean(y_points))\n",
    "        # avoid to beyond boundaries of array\n",
    "        x = max(56,min(x,167))\n",
    "        y = max(56,min(y,167))\n",
    "        # get cropped video \n",
    "        return video[:,x-56:x+56,y-56:y+56,:]  \n",
    "        \n",
    "    def load_data(self, path):\n",
    "        # just load the last two channels containing optical flows\n",
    "        data = np.load(path, mmap_mode='r')[...,3:]\n",
    "        data = np.float32(data)\n",
    "        # sampling 64 frames uniformly from the entire video\n",
    "        data = self.uniform_sampling(video=data, target_frames=64)\n",
    "        # normalize the optical flows\n",
    "        data = self.normalize(data)\n",
    "        return data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "tiRBuTO8m0yY"
   },
   "source": [
    "### Build Model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:34:00.246008Z",
     "start_time": "2020-04-03T12:34:00.239310Z"
    },
    "colab": {},
    "colab_type": "code",
    "id": "FfP2e_X-m0ya"
   },
   "outputs": [],
   "source": [
    "from keras.models import Sequential, Input, Model\n",
    "from keras.layers import Dense, Flatten, Conv3D, MaxPooling3D, Dropout, BatchNormalization, Activation, LeakyReLU, Add, Multiply\n",
    "from keras.regularizers import l2\n",
    "from keras.layers.core import Lambda\n",
    "from keras.layers.core import Lambda"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:34:01.359781Z",
     "start_time": "2020-04-03T12:34:00.939052Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Model: \"model_1\"\n",
      "_________________________________________________________________\n",
      "Layer (type)                 Output Shape              Param #   \n",
      "=================================================================\n",
      "input_1 (InputLayer)         (None, 64, 224, 224, 2)   0         \n",
      "_________________________________________________________________\n",
      "conv3d_1 (Conv3D)            (None, 64, 224, 224, 16)  304       \n",
      "_________________________________________________________________\n",
      "conv3d_2 (Conv3D)            (None, 64, 224, 224, 16)  784       \n",
      "_________________________________________________________________\n",
      "max_pooling3d_1 (MaxPooling3 (None, 64, 112, 112, 16)  0         \n",
      "_________________________________________________________________\n",
      "conv3d_3 (Conv3D)            (None, 64, 112, 112, 16)  2320      \n",
      "_________________________________________________________________\n",
      "conv3d_4 (Conv3D)            (None, 64, 112, 112, 16)  784       \n",
      "_________________________________________________________________\n",
      "max_pooling3d_2 (MaxPooling3 (None, 64, 56, 56, 16)    0         \n",
      "_________________________________________________________________\n",
      "conv3d_5 (Conv3D)            (None, 64, 56, 56, 32)    4640      \n",
      "_________________________________________________________________\n",
      "conv3d_6 (Conv3D)            (None, 64, 56, 56, 32)    3104      \n",
      "_________________________________________________________________\n",
      "max_pooling3d_3 (MaxPooling3 (None, 64, 28, 28, 32)    0         \n",
      "_________________________________________________________________\n",
      "conv3d_7 (Conv3D)            (None, 64, 28, 28, 32)    9248      \n",
      "_________________________________________________________________\n",
      "conv3d_8 (Conv3D)            (None, 64, 28, 28, 32)    3104      \n",
      "_________________________________________________________________\n",
      "max_pooling3d_4 (MaxPooling3 (None, 64, 14, 14, 32)    0         \n",
      "_________________________________________________________________\n",
      "max_pooling3d_5 (MaxPooling3 (None, 8, 14, 14, 32)     0         \n",
      "_________________________________________________________________\n",
      "conv3d_9 (Conv3D)            (None, 8, 14, 14, 64)     18496     \n",
      "_________________________________________________________________\n",
      "conv3d_10 (Conv3D)           (None, 8, 14, 14, 64)     12352     \n",
      "_________________________________________________________________\n",
      "max_pooling3d_6 (MaxPooling3 (None, 4, 7, 7, 64)       0         \n",
      "_________________________________________________________________\n",
      "conv3d_11 (Conv3D)           (None, 4, 7, 7, 64)       36928     \n",
      "_________________________________________________________________\n",
      "conv3d_12 (Conv3D)           (None, 4, 7, 7, 64)       12352     \n",
      "_________________________________________________________________\n",
      "max_pooling3d_7 (MaxPooling3 (None, 2, 3, 3, 64)       0         \n",
      "_________________________________________________________________\n",
      "conv3d_13 (Conv3D)           (None, 2, 3, 3, 128)      73856     \n",
      "_________________________________________________________________\n",
      "conv3d_14 (Conv3D)           (None, 2, 3, 3, 128)      49280     \n",
      "_________________________________________________________________\n",
      "max_pooling3d_8 (MaxPooling3 (None, 1, 1, 1, 128)      0         \n",
      "_________________________________________________________________\n",
      "flatten_1 (Flatten)          (None, 128)               0         \n",
      "_________________________________________________________________\n",
      "dense_1 (Dense)              (None, 128)               16512     \n",
      "_________________________________________________________________\n",
      "dropout_1 (Dropout)          (None, 128)               0         \n",
      "_________________________________________________________________\n",
      "dense_2 (Dense)              (None, 32)                4128      \n",
      "_________________________________________________________________\n",
      "dense_3 (Dense)              (None, 2)                 66        \n",
      "=================================================================\n",
      "Total params: 248,258\n",
      "Trainable params: 248,258\n",
      "Non-trainable params: 0\n",
      "_________________________________________________________________\n"
     ]
    }
   ],
   "source": [
    "inputs = Input(shape=(64,224,224,2))\n",
    "\n",
    "##################################################### \n",
    "opt = inputs\n",
    "opt = Conv3D(\n",
    "    16, kernel_size=(1,3,3), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = Conv3D(\n",
    "    16, kernel_size=(3,1,1), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = MaxPooling3D(pool_size=(1,2,2))(opt)\n",
    "\n",
    "opt = Conv3D(\n",
    "    16, kernel_size=(1,3,3), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = Conv3D(\n",
    "    16, kernel_size=(3,1,1), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = MaxPooling3D(pool_size=(1,2,2))(opt)\n",
    "\n",
    "opt = Conv3D(\n",
    "    32, kernel_size=(1,3,3), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = Conv3D(\n",
    "    32, kernel_size=(3,1,1), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = MaxPooling3D(pool_size=(1,2,2))(opt)\n",
    "\n",
    "opt = Conv3D(\n",
    "    32, kernel_size=(1,3,3), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = Conv3D(\n",
    "    32, kernel_size=(3,1,1), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(opt)\n",
    "opt = MaxPooling3D(pool_size=(1,2,2))(opt)\n",
    "\n",
    "##################################################### \n",
    "x = MaxPooling3D(pool_size=(8,1,1))(opt)\n",
    "\n",
    "##################################################### \n",
    "x = Conv3D(\n",
    "    64, kernel_size=(1,3,3), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(x)\n",
    "x = Conv3D(\n",
    "    64, kernel_size=(3,1,1), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(x)\n",
    "x = MaxPooling3D(pool_size=(2,2,2))(x)\n",
    "\n",
    "x = Conv3D(\n",
    "    64, kernel_size=(1,3,3), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(x)\n",
    "x = Conv3D(\n",
    "    64, kernel_size=(3,1,1), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(x)\n",
    "x = MaxPooling3D(pool_size=(2,2,2))(x)\n",
    "\n",
    "x = Conv3D(\n",
    "    128, kernel_size=(1,3,3), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(x)\n",
    "x = Conv3D(\n",
    "    128, kernel_size=(3,1,1), strides=(1,1,1), kernel_initializer='he_normal', activation='relu', padding='same')(x)\n",
    "x = MaxPooling3D(pool_size=(2,3,3))(x)\n",
    "\n",
    "#####################################################\n",
    "x = Flatten()(x)\n",
    "x = Dense(128,activation='relu')(x)\n",
    "x = Dropout(0.2)(x)\n",
    "x = Dense(32, activation='relu')(x)\n",
    "pred = Dense(2, activation='softmax')(x)\n",
    "model = Model(inputs=inputs, outputs=pred)\n",
    "\n",
    "model.summary()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "2TZgW6QR8oqH"
   },
   "source": [
    "### Set Callbacks"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Learning Rate Scheduler"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:34:02.692919Z",
     "start_time": "2020-04-03T12:34:02.683511Z"
    }
   },
   "outputs": [],
   "source": [
    "import keras.backend as K\n",
    "from keras.callbacks import LearningRateScheduler\n",
    "\n",
    "def scheduler(epoch):\n",
    "    if epoch % 10 == 0 and epoch != 0:\n",
    "        lr = K.get_value(parallel_model.optimizer.lr)\n",
    "        K.set_value(parallel_model.optimizer.lr, lr * 0.5)\n",
    "    return K.get_value(parallel_model.optimizer.lr)\n",
    "\n",
    "reduce_lr = LearningRateScheduler(scheduler)\n",
    "callbacks_list = [reduce_lr]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Set the GPUs and make it parallel"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:34:05.731858Z",
     "start_time": "2020-04-03T12:34:03.671992Z"
    }
   },
   "outputs": [],
   "source": [
    "os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"0,1,2,3\"\n",
    "\n",
    "from keras.utils import multi_gpu_model   \n",
    "parallel_model = multi_gpu_model(model, gpus=4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "LBWQTN6Mm0ym"
   },
   "source": [
    "### Model Compiling"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:34:05.789495Z",
     "start_time": "2020-04-03T12:34:05.743985Z"
    },
    "colab": {},
    "colab_type": "code",
    "id": "EGID3emEm0yn"
   },
   "outputs": [],
   "source": [
    "from keras.optimizers import Adam, SGD\n",
    "\n",
    "sgd = SGD(lr=0.01, decay=1e-6, momentum=0.9, nesterov=True)\n",
    "parallel_model.compile(optimizer=sgd, loss='categorical_crossentropy', metrics=['accuracy'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "colab_type": "text",
    "id": "DG5y04tym0yr"
   },
   "source": [
    "### Model Training"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- set essential params"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:34:06.132117Z",
     "start_time": "2020-04-03T12:34:06.129121Z"
    }
   },
   "outputs": [],
   "source": [
    "num_epochs  = 30\n",
    "num_workers = 16\n",
    "batch_size  = 16"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- init the data generator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-04-03T12:34:12.885055Z",
     "start_time": "2020-04-03T12:34:12.864920Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Found 1600 files belonging to 2 classes.\n",
      "     Fight :  0\n",
      "  NonFight :  1\n",
      "Found 400 files belonging to 2 classes.\n",
      "     Fight :  0\n",
      "  NonFight :  1\n"
     ]
    }
   ],
   "source": [
    "dataset = 'RWF2000-opt'\n",
    "\n",
    "train_generator = DataGenerator(directory='../Datasets/{}/train'.format(dataset), \n",
    "                                batch_size=batch_size, \n",
    "                                data_augmentation=True)\n",
    "\n",
    "val_generator = DataGenerator(directory='../Datasets/{}/val'.format(dataset),\n",
    "                              batch_size=batch_size, \n",
    "                              data_augmentation=False)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- start to train"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "ExecuteTime": {
     "end_time": "2020-03-25T17:29:00.065904Z",
     "start_time": "2020-03-25T16:28:23.318597Z"
    },
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 52
    },
    "colab_type": "code",
    "executionInfo": {
     "elapsed": 40622,
     "status": "error",
     "timestamp": 1561260510157,
     "user": {
      "displayName": "XXXXg CXXXX",
      "photoUrl": "",
      "userId": "04701665026083715772"
     },
     "user_tz": -480
    },
    "id": "wTd5cDHBm0yt",
    "outputId": "099af51f-6278-4284-db46-6ade4b7a9fda"
   },
   "outputs": [],
   "source": [
    "hist = parallel_model.fit_generator(\n",
    "    generator=train_generator, \n",
    "    validation_data=val_generator,\n",
    "    callbacks=callbacks_list,\n",
    "    verbose=1, \n",
    "    epochs=num_epochs,\n",
    "    workers=num_workers ,\n",
    "    max_queue_size=8,\n",
    "    steps_per_epoch=len(train_generator),\n",
    "    validation_steps=len(val_generator))"
   ]
  }
 ],
 "metadata": {
  "accelerator": "GPU",
  "colab": {
   "collapsed_sections": [],
   "name": "C3D.ipynb",
   "provenance": [],
   "version": "0.3.2"
  },
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
